package com.gframework.boot.mvc.filter;

import java.io.IOException;
import java.util.regex.Pattern;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;

import com.gframework.boot.mvc.filter.CSRFSafeFilter.CSRFSafeProperties;

/**
 * 一个用于处理csrf攻击的过滤器(此filter最后执行).
 * <p>
 * <strong>CSRF</strong><br>
 * CSRF是一个跨站脚本攻击，区别于XSS，他是在其他网站对特定网站进行攻击，依赖于你已经对这个网站进行了登录而所拥有的cookie。
 * <p>
 * 一般来说，只要使用正常的浏览器，并且网站拒绝一切跨域请求，那么是几乎不会存在CSRF攻击的，仅仅要做的就是不要将敏感操作放到get请求上，
 * 因为大量的html标签都会提供一个隐藏的不会被跨域拦截的get请求。
 * <p>
 * 跨域限制与否主要是在于浏览器而不是服务端，如果不能够保证跨域限制或者需要开启跨域访问，那么就可以使用此安全组件。
 * 此安全组件要求客户端发起任何请求之前，需要在cookie和header中同时存在一个token，这个token可以由前端生成，但是必须是随机的，不可预测的。
 * <p>
 * token没有必要后端生成是因为跨域攻击是无法操作攻击域cookie的，如果能够获取到，那就说明网站存在XSS漏洞，如果存在XSS漏洞，那无论以何种方式生成token都是无效的了。
 * <p>
 * 但此过滤器也没有完全限制token只能由前台生成，实际上无论是通过何种方式生成的都是可以的。
 * 
 * @since 2.0.0
 * @author Ghwolf
 * @see CSRFErrorProcessor
 */
@ConditionalOnProperty(value = "gframework.mvc.filter.csrf.enabled", havingValue = "true")
@EnableConfigurationProperties(CSRFSafeProperties.class)
@Order(Integer.MAX_VALUE)
public class CSRFSafeFilter extends OncePerRequestFilter {

	public static final String DEFAULT_CSRF_COOKIE_HEADER_NAME = "x-csrf-token";
	public static final String DEFAULT_IGNORE_REGEX = "(.js|.css|.png|.jpg|.jpeg|.bmp|.gif|.ico|.woff|.woff2|.svg|.eof|.map)$";
	
	/**
	 * 用于匹配路径的正则对象
	 */
	private Pattern pattern;
	
	@Autowired
	private CSRFSafeProperties csrfSafeProperties ;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		if (pattern.matcher(request.getRequestURI()).matches()) {
			filterChain.doFilter(request, response);
			return;
		}

		if (verifyCsrf(request)) {
			filterChain.doFilter(request, response);
		} else {
			if (logger.isDebugEnabled()) {
				logger.debug("CSRF验证失败，请求地址为：" + request.getRequestURI());
			}
			if (csrfSafeProperties.csrfErrorProcessor == null || !csrfSafeProperties.csrfErrorProcessor.handle(request, response) ) {
				HttpStatus hs = HttpStatus.PRECONDITION_FAILED;
				response.sendError(hs.value(), hs.getReasonPhrase());
			}
		}

	}

	/**
	 * 验证csrf
	 */
	private boolean verifyCsrf(HttpServletRequest request) {

		Cookie csrfCookie = WebUtils.getCookie(request, csrfSafeProperties.csrfCookieName);
		String csrfValue = request.getHeader(csrfSafeProperties.csrfHeaderName);
		if (csrfCookie == null) {
			return false;
		}
		if (csrfValue == null) {
			csrfValue = request.getParameter(csrfSafeProperties.csrfParamName);
		}
		if (csrfValue == null) {
			return false ;
		}
		
		return csrfValue.equals(csrfCookie.getValue());
	}

	@Override
	public void afterPropertiesSet() throws ServletException {
		this.pattern = Pattern.compile(csrfSafeProperties.ignoreRegex, Pattern.CASE_INSENSITIVE);
		logger.info("====> 以启用 CSRFSafeFilter 安全过滤器，用于处理 CSRF 安全问题。");
		super.afterPropertiesSet();
	}
	
	@ConfigurationProperties("gframework.mvc.filter.csrf")
	static class CSRFSafeProperties {
		/**
		 * 是否开启csrf安全过滤器，默认关闭(false)
		 */
		private boolean enabled = false;
		/**
		 * 用于将随机csrf
		 * token字符串存储到客户端cookie中的名称，默认是：x-csrf-token
		 */
		private String csrfCookieName = DEFAULT_CSRF_COOKIE_HEADER_NAME;
		/**
		 * 请求时需要在header中携带csrf随机key的header名称，默认是：x-csrf-token
		 * <p>
		 * 此项比{@link #csrfParamName}优先级更高
		 */
		private String csrfHeaderName = DEFAULT_CSRF_COOKIE_HEADER_NAME;
		/**
		 * 请求时需要在请求参数中携带csrf随机key的参数名称，默认是：x-csrf-token
		 * <p>
		 * 请求content-type必须是key-value格式的，且此项比{@link #csrfHeaderName}优先级更低
		 */
		private String csrfParamName = DEFAULT_CSRF_COOKIE_HEADER_NAME;
		/**
		 * 忽略的url正则匹配，建议使用类似于：“(.js|.css|.png)$”这样的形式，以减少正则回溯获得最大性能
		 */
		private String ignoreRegex = DEFAULT_IGNORE_REGEX;
		/**
		 * csrf 验证异常处理器，你可以自定义处理器以返回正确的结果给客户端
		 */
		private CSRFErrorProcessor csrfErrorProcessor;
		
		public CSRFErrorProcessor getCsrfErrorProcessor() {
			return this.csrfErrorProcessor;
		}

		public void setCsrfErrorProcessor(CSRFErrorProcessor csrfErrorProcessor) {
			this.csrfErrorProcessor = csrfErrorProcessor;
		}
		
		public boolean isEnabled() {
			return this.enabled;
		}

		public void setEnabled(boolean enabled) {
			this.enabled = enabled;
		}

		public String getCsrfCookieName() {
			return this.csrfCookieName;
		}

		public void setCsrfCookieName(String csrfCookieName) {
			this.csrfCookieName = csrfCookieName;
		}

		public String getCsrfHeaderName() {
			return this.csrfHeaderName;
		}

		public void setCsrfHeaderName(String csrfHeaderName) {
			this.csrfHeaderName = csrfHeaderName;
		}

		public String getCsrfParamName() {
			return this.csrfParamName;
		}

		public void setCsrfParamName(String csrfParamName) {
			this.csrfParamName = csrfParamName;
		}

		public String getIgnoreRegex() {
			return this.ignoreRegex;
		}

		public void setIgnoreRegex(String ignoreRegex) {
			this.ignoreRegex = ignoreRegex;
		}
		
	}

}
